﻿using System;
using System.Text;

namespace SkeinLib
{
    public static partial class SkeinCore
    {
        // all Tree Hashing specific functions are broken out here for clarity
        // this does not include Config or Initialize as those functions are used for both Tree and Straight hashing

        /// <summary>
        /// Copy a portion of a byte array for processing.
        /// </summary>
        /// <param name="theArray">The source array.</param>
        /// <param name="startPos">The start position in the array.</param>
        /// <param name="length">The number of bytes starting at the start position to copy.</param>
        /// <returns>The copied bytes.</returns>
        internal static byte[] ExtractArray(ref byte[] theArray, int startPos, int length)
        {
            byte[] result = new byte[length];
            Buffer.BlockCopy(theArray, startPos, result, 0, result.Length);
            return result;
        }

#warning "This function may be updated to a Thread-based function with callback.  You have been warned."
        /// <summary>
        /// 
        /// </summary>
        /// <param name="state"></param>
        /// <param name="data"></param>
        /// <param name="startPos"></param>
        /// <param name="endPos"></param>
        /// <param name="lastByteBitCount"></param>
        private static void ProcessTreeLeaf(ref UInt64[] state, ref byte[] data, int startPos, int endPos, byte lastByteBitCount)
        {   // this is the first function in the tree hashing stack
            // startPos and endPos are checked by the caller!!!!

            UInt64[] transport = new UInt64[state.Length];
            UInt64[] tweak = { 0, SKEIN_FLAG_FIRST + SKEIN_TYPE_MSG + SKEIN_TREELEVEL_MULTIPLIER };

            int stateByteWidth = Buffer.ByteLength(state);
            int blockWidth = endPos - startPos;

            for (int i = 0; (i * stateByteWidth) < blockWidth || (i == 0 && blockWidth == 0); i++)
            {
                //process a block  
                int currentPosition = i * stateByteWidth;
                tweak[0] = (UInt64)(currentPosition + startPos);
                if (currentPosition + stateByteWidth >= blockWidth)
                {
                    ClearWords(ref transport);
                    tweak[0] = (UInt64)endPos;

                    tweak[1] += SKEIN_FLAG_FINAL;
                    if (lastByteBitCount > 0)
                    {
                        tweak[1] += SKEIN_FLAG_PAD;
                        Buffer.BlockCopy(data, currentPosition + startPos, transport, 0, (blockWidth - currentPosition) - 1);
                        byte newLastByte = data[blockWidth + startPos - 1];
                        newLastByte &= (byte)(0xFF << (8 - lastByteBitCount));
                        newLastByte += (byte)(Math.Pow(2, (7 - lastByteBitCount)));
                        int newLastBytePosition = (blockWidth - 1 + Buffer.ByteLength(transport)) % Buffer.ByteLength(transport);

                        Buffer.SetByte(transport, newLastBytePosition, newLastByte);
                    }
                    else
                        Buffer.BlockCopy(data, currentPosition + startPos, transport, 0, blockWidth - currentPosition);

                }
                else
                {
                    tweak[0] += (UInt64)stateByteWidth;
                    Buffer.BlockCopy(data, currentPosition + startPos, transport, 0, stateByteWidth);
                }
                //now transform
                switch (state.Length)
                {
                    case 4: ThreeFish.ThreeFish256Words(transport, state, tweak, true, ref state); break;
                    case 8: ThreeFish.ThreeFish512Words(transport, state, tweak, true, ref state); break;
                    case 16: ThreeFish.ThreeFish1024Words(transport, state, tweak, true, ref state); break;
                }
                tweak[1] &= ~SKEIN_FLAG_FIRST;
            }
            ClearWords(ref transport, true);
            ClearWords(ref tweak, true);
            //TODO:  When this becomes a threaded function, do the callback magic here with "state"
            return;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="state"></param>
        /// <param name="data"></param>
        /// <param name="level"></param>
        /// <param name="lengthOffset"></param>
        private static void ProcessTreeNode(ref UInt64[] state, ref UInt64[] data, uint level, UInt64 lengthOffset)
        {
            UInt64[] transport = new UInt64[state.Length];
            UInt64[] tweak = { lengthOffset, SKEIN_FLAG_FIRST + SKEIN_TYPE_MSG + (SKEIN_TREELEVEL_MULTIPLIER * level) };

            int stateByteWidth = Buffer.ByteLength(state);
            int dataByteWidth = Buffer.ByteLength(data);

            for (int i = 0; i * stateByteWidth < dataByteWidth; i++)
            {
                //process a block  
                tweak[0] += (UInt64)stateByteWidth;
                if (((i + 1) * stateByteWidth) >= dataByteWidth)
                    tweak[1] += SKEIN_FLAG_FINAL;
                Buffer.BlockCopy(data, i * stateByteWidth, transport, 0, stateByteWidth);
                //now transform
                //SkeinDebug.WriteData2File(ref tweak, "debug", "ProcessTreeNodeTWEAK, lengthOffset=" + lengthOffset.ToString() + " i=" + i.ToString(),
                //    "The transport of the node being processed at i=" + i.ToString());
                //SkeinDebug.WriteData2File(ref state, "debug", "ProcessTreeNodeSTATE, lengthOffset=" + lengthOffset.ToString() + " i=" + i.ToString(),
                //    "The transport of the node being processed at i=" + i.ToString());
                switch (state.Length)
                {
                    case 4: ThreeFish.ThreeFish256Words(transport, state, tweak, true, ref state); break;
                    case 8: ThreeFish.ThreeFish512Words(transport, state, tweak, true, ref state); break;
                    case 16: ThreeFish.ThreeFish1024Words(transport, state, tweak, true, ref state); break;
                }
                tweak[1] &= ~SKEIN_FLAG_FIRST;
            }
            ClearWords(ref transport, true);
            ClearWords(ref tweak, true);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="state"></param>
        /// <param name="data"></param>
        /// <param name="treeLeaf"></param>
        /// <param name="treeNode"></param>
        /// <param name="treeMaxLevels"></param>
        public static void ProcessMessage(ref UInt64[] state, byte[] data, byte treeLeaf, byte treeNode, byte treeMaxLevels)
        {
            //use this method call for a data length that does not contain partial bits
            ProcessMessage(ref state, data, 0, treeLeaf, treeNode, treeMaxLevels);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="state"></param>
        /// <param name="data"></param>
        /// <param name="lastByteBitCount"></param>
        /// <param name="treeLeaf"></param>
        /// <param name="treeNode"></param>
        /// <param name="treeMaxLevels"></param>
        public static void ProcessMessage(ref UInt64[] state, byte[] data, byte lastByteBitCount, byte treeLeaf, byte treeNode, byte treeMaxLevels)
        {
            if (lastByteBitCount > 7)
                throw new ArgumentOutOfRangeException("lastByteBitCount", "ProcessMessage:  the last byte count cannot exceed 7 bits. " +
                    "If the last byte is complete (no padding), pass zero.");
            if (treeMaxLevels < 2 && treeLeaf > 0 && treeNode > 0)
                throw new ArgumentOutOfRangeException("treeMaxLevels", "ProcessMessage:  treeMaxLevels must be greater than 1 when doing tree hashing.");
            //SkeinDebug.WriteData2File(ref state, "debug", "statePREPROCESS", "This is the state BEFORE ANYTHING IS DONE TO THE MESSAGE!");
            UInt64[] stateTemp;
            int stateByteSize = Buffer.ByteLength(state);
            int leafQWordSize = (int)Math.Pow(2, treeLeaf);
            int leafByteSize = leafQWordSize * stateByteSize;
            int level1NodeCount = (int)Math.Ceiling((double)data.Length / leafByteSize);

            int startingPosition = 0;
            int endingPosition = 0;

            //do leaves of level 1 first straight through
            UInt64[] leaves = new UInt64[level1NodeCount * state.Length];
            for (int i = 0; i < level1NodeCount; i++)
            {
                startingPosition = i * leafByteSize;
                if (startingPosition + leafByteSize >= data.Length) // near the end?
                    endingPosition = startingPosition + (data.Length - startingPosition);  // yes
                else
                    endingPosition = startingPosition + leafByteSize;  // no, in the middle or end block exact size
                stateTemp = (UInt64[])state.Clone();
                //TODO:  Make this a thread call!            
                ProcessTreeLeaf(ref stateTemp, ref data, startingPosition, endingPosition, (byte)(i == level1NodeCount - 1 ? lastByteBitCount : 0));
                Buffer.BlockCopy(stateTemp, 0, leaves, stateByteSize * i, stateByteSize);
                //pump stateTemp into leaves here <================
                ClearWords(ref stateTemp);

            }
            //TODO:  Join the threads here before moving up the tree
            //SkeinDebug.WriteData2File(ref leaves, "debug", "leavesafterlevelONE", "These are the leaves after LEVEL ONE processing.");
            int currentLevel = 2;
            UInt64[] workingNodes = (UInt64[])leaves.Clone();
            ClearWords(ref leaves);
            do
            {
                if (workingNodes.Length == state.Length)
                {
                    ClearWords(ref state);
                    state = (UInt64[])workingNodes.Clone();
                    ClearWords(ref workingNodes); // clean up memory
                    //SkeinDebug.WriteData2File(ref state,"debug", "leaves.lengthEQstate.length", "This is the state after the last process when the leaves are the correct length from the previous run.");
                    return; // complete!  We only needed one level of tree hashing to accomplish the task!
                }
                else
                {
                    int nodeQWordSize = state.Length * (int)Math.Pow(2, (double)treeNode);
                    int nodeByteCount = nodeQWordSize * sizeof(UInt64);
                    if (currentLevel == treeMaxLevels)
                    {
                        stateTemp = (UInt64[])state.Clone();
                        ProcessTreeNode(ref stateTemp, ref workingNodes, (uint)currentLevel, 0);
                        ClearWords(ref workingNodes);
                        ClearWords(ref state);
                        state = (UInt64[])stateTemp.Clone();
                        ClearWords(ref stateTemp);
                        //SkeinDebug.WriteData2File(ref state, "debug", "currentLevelEQtreeMaxLevels", "This is the state after the last process when the maxTreeLevels threshold is hit.");
                        return; // complete!  We topped-out on the tree (upper level constraint) 
                    }
                    else
                    {
                        int intPutNodeCount = workingNodes.Length / state.Length;
                        int outPutNodeCount = (int)Math.Ceiling(intPutNodeCount / Math.Pow(2, treeNode));
                        int currentOutPutNode = 0;
                        UInt64[] outPutNodes = new UInt64[outPutNodeCount * state.Length];
                        for (int i = 0; i < workingNodes.Length; i += nodeQWordSize)
                        {
                            stateTemp = (UInt64[])state.Clone();
                            UInt64[] tempNode;
                            if (workingNodes.Length - i >= nodeQWordSize)
                            {   // still working with full nodes
                                tempNode = new UInt64[nodeQWordSize];
                                Buffer.BlockCopy(workingNodes, currentOutPutNode * nodeByteCount, tempNode, 0, nodeByteCount);
                            }
                            else
                            {   // got a node that isn't the full size
                                tempNode = new UInt64[workingNodes.Length - i];
                                Buffer.BlockCopy(workingNodes, currentOutPutNode * nodeByteCount, tempNode, 0, Buffer.ByteLength(tempNode));
                                //throw new Exception("Code Stop Here for Problem in SkeinLib.SkeinCore.ProcessMessage()");
                            }
                            ProcessTreeNode(ref stateTemp, ref tempNode, (uint)currentLevel, (UInt64)(currentOutPutNode * nodeByteCount));
                            //SkeinDebug.WriteData2File(ref stateTemp, "debug", "stateTemp", "This is the state after the last process." +
                            //    " currentOutPutNode=" + currentOutPutNode.ToString() + 
                            //    " currentLevel=" + currentLevel.ToString());
                            Buffer.BlockCopy(stateTemp, 0, outPutNodes, stateByteSize * currentOutPutNode++, stateByteSize);
                            ClearWords(ref stateTemp);
                            ClearWords(ref tempNode);
                        }
                        workingNodes = (UInt64[])outPutNodes.Clone();
                        ClearWords(ref outPutNodes);
                    }
                }
            } while (currentLevel++ <= treeMaxLevels);

        }
    }
}
